/**
 * Copyright (C) 2010 Hamburg Sud
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.hamburgsud.log4testing.core.data;

import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.dom4j.Document;
import org.dom4j.Node;
import org.joda.time.DateTime;

import com.hamburgsud.log4testing.core.configuration.Log4TestingConfiguration;
import com.hamburgsud.log4testing.core.data.attachment.Attachment;

/**
 * This class stores all the data which corresponds to a test step. Which data
 * is to be stored in a test step is to be decided by test writers themselves.
 * That means that test writers can decide if they want log technical or domain
 * driven commands or for instance which messages will be provided by their
 * implementations of their commands after execution. The starting time of a
 * test step will be automatically set when a new instance of this test step is
 * created.
 * <p>
 * In addition to all the mentioned information every test step has its unique
 * id which is unique in the context of the JVM in which the test step is
 * created. Ids can change across two executions. This means that the id of a
 * test step can change with the next execution.
 * </p>
 * 
 * @author
 * 
 */
public class TestStep extends TestObject {

	// TODO Move the following XPath expressions for the access of
	// configuration values to a central configuration helper class. Do the same
	// with the two lists of patterns.
	private static final String XPATH_CALLER_CLASS = "/log4testing/caller//class";
	private static final String XPATH_CALLER_METHOD = "/log4testing/caller//method";
	private static List<Pattern> callerClasses = null;
	private static List<Pattern> callerMethods = null;

	/**
	 * Date and time when the execution of this {@link TestStep} was started.
	 */
	private DateTime startingTime = null;

	/**
	 * Date and time when the execution of this {@link TestStep} was finished.
	 */
	private DateTime finishingTime = null;

	/**
	 * Command which shall be logged with this {@link TestStep}. The command is
	 * usually a test command itself.
	 */
	private String command = null;

	/**
	 * A message provides some textual information about the execution of the
	 * command.
	 */
	private String message = null;

	/**
	 * A stacktrace of callers which have called the {@link #finish()} method.
	 */
	private String[] caller = null;

	/**
	 * Status of this {@link TestStep}. The initial status of a {@link TestStep}
	 * is {@link TestStatus#UNINITIALIZED} and is to be set.
	 */
	private TestStatus status = TestStatus.UNINITIALIZED;

	/**
	 * List of attachments which provide more information about this
	 * {@link TestStep}.
	 */
	private ArrayList<Attachment> attachments = new ArrayList<Attachment>();

	/**
	 * Constructs a new TestStep and starts it through a call of the method
	 * {@link #start()}.
	 */
	public TestStep() {
		super();
		start();
	}

	/**
	 * Name of the command to which this {@link TestStep} corresponds. The
	 * returned command is not more than just a simple name.
	 * 
	 * @return the command for which this {@link TestStep} provides some test
	 *         logging information. Returns <code>null</code> if it wasn't set.
	 */
	public String getCommand() {
		return command;
	}

	/**
	 * Is used to set the name of the command logged by this {@link TestStep}.
	 * 
	 * @param command
	 *            is the name of the command which were executed and which shall
	 *            be logged with the help of this {@link TestStep} object
	 * @return the {@link TestStep} itself so that it can be used for some other
	 *         method calls
	 */
	public TestStep setCommand(final String command) {
		this.command = command;
		return this;
	}

	/**
	 * Clears the command and sets it to <code>null</code>.
	 */
	public TestStep clearCommand() {
		return setCommand(null);
	}

	// TODO use this for the attachment method
	/**
	 * {@link TestStep}s can provide more information through attachments which
	 * itself can be text files, pictures, binary files or something else.
	 * Purpose of these attachments is, that more information about the object
	 * under test can be provided. Simple messages are not enough sometimes and
	 * additional information is needed.</br> Tip: Very helpful
	 */

	/**
	 * Is used to set a message which comes from the code or test itself and
	 * which shall be logged. This might be when a test step gets executed and
	 * the result shall be logged.
	 * 
	 * @return <code>null</code> if there is no message available. It returns a
	 *         message string if a message has been set.
	 */
	public String getMessage() {
		return message;
	}

	/**
	 * Clears the message and sets it to <code>null</code>.
	 */
	public TestStep clearMessage() {
		return setMessage(null);
	}

	/**
	 * @param message
	 *            the message to set
	 */
	public TestStep setMessage(final String message) {
		this.message = message;
		return this;
	}

	/**
	 * @return the status
	 */
	public TestStatus getStatus() {
		return status;
	}

	/**
	 * @param status
	 *            the status to set
	 */
	public TestStep setStatus(final TestStatus status) {
		this.status = status;
		finish();
		return this;
	}

	/**
	 * Adds an attachment and associates it with this test step.
	 * 
	 * @param attachment
	 *            which shall be associated witht this test step
	 */
	public TestStep addAttachment(final Attachment attachment) {
		attachments.add(attachment);
		return this;
	}

	/**
	 * Removes all associations of this test step with attachments.
	 */
	public TestStep clearAttachments() {
		attachments.removeAll(attachments);
		return this;
	}

	/**
	 * @return the attachedFiles
	 */
	public Iterable<Attachment> getAttachments() {
		return attachments;
	}

	/**
	 * 
	 * @return the starting Time
	 */
	public TestStep start() {
		startingTime = new DateTime();
		return this;
	}

	/**
	 * Finishes a TestStep by setting the <code> finishingTime </code>
	 * 
	 * @return
	 */
	public TestStep finish() {
		finishingTime = new DateTime();
		setCaller();
		return this;
	}

	/**
	 * Every TestStep saves the complete calling hierarchy when the method
	 * {@link #finish()} gets called. A filter can be configured so that only
	 * the caller methods will be saved which are of interest (s.
	 * log4testing.xml).<br/>
	 * As follows a example configuration which shows a filter which will accept
	 * each and every class and method.
	 * 
	 * <pre>
	 * <code>
	 * &lt;log4testing&gt;
	 * ...
	 *   &lt;caller&gt;
	 *     &lt;class&gt;.*&lt;/class&gt;
	 *     &lt;method&gt;.*&lt;/method&gt;
	 *   &lt;/caller&gt;
	 * ...
	 * &lt;/log4testing&gt; 
	 * </code>
	 * </pre>
	 * 
	 * These filters are a white list and every method and class which match one
	 * of these pattern will be logged. Patterns here must be regular
	 * expressions.
	 * 
	 * @return a list of all the callers which match the configured class and
	 *         method patterns in log4testing.xml
	 */
	public String[] getCaller() {
		if (caller == null) {
			caller = new String[0];
		}
		return caller;
	}

	private void setCaller() {
		final ArrayList<String> caller = new ArrayList<String>();
		final StackTraceElement[] traceElements = Thread.currentThread()
				.getStackTrace();
		for (final StackTraceElement traceElement : traceElements) {
			addCaller(caller, traceElement);
		}
		this.caller = caller.toArray(new String[0]);
	}

	private void addCaller(final ArrayList<String> caller,
			final StackTraceElement traceElement) {
		matchClasses(caller, traceElement);
	}

	private void matchClasses(final ArrayList<String> caller,
			final StackTraceElement traceElement) {
		final List<Pattern> classes = getCallerClasses();
		for (final Pattern classPattern : classes) {
			final Matcher classMatcher = classPattern.matcher(traceElement
					.getClassName());
			if (classMatcher.matches()) {
				matchMethods(caller, traceElement);
			}
		}
	}

	private void matchMethods(final ArrayList<String> caller,
			final StackTraceElement traceElement) {
		final List<Pattern> methods = getCallerMethods();
		for (final Pattern methodPattern : methods) {
			final Matcher methodMatcher = methodPattern.matcher(traceElement
					.getMethodName());
			if (methodMatcher.matches()) {
				caller.add(traceElement.toString());
			}
		}
	}

	private static List<Pattern> getCallerClasses() {
		if (callerClasses == null) {
			callerClasses = getPatterns(XPATH_CALLER_CLASS);
		}
		return callerClasses;
	}

	private static List<Pattern> getCallerMethods() {
		if (callerMethods == null) {
			callerMethods = getPatterns(XPATH_CALLER_METHOD);
		}
		return callerMethods;
	}

	@SuppressWarnings("unchecked")
	synchronized private static List<Pattern> getPatterns(final String xpath) {
		final Document config = Log4TestingConfiguration.getConfig();
		final List<Node> expressions = config.selectNodes(xpath);
		final ArrayList<Pattern> patterns = new ArrayList<Pattern>(
				expressions.size());
		for (final Node expression : expressions) {
			final Pattern pattern = Pattern.compile(expression.getText());
			patterns.add(pattern);
		}
		return patterns;
	}

	/**
	 * @return the finishing time
	 */
	@Override
	public DateTime getFinishingTime() {
		return finishingTime;
	}

	/**
	 * @return the starting time
	 */
	@Override
	public DateTime getStartingTime() {
		return startingTime;
	}

	/**
	 * Checks if the TestStep is failed
	 */
	public boolean isFailed() {
		boolean failed = false;
		switch (getStatus()) {
		case ERROR:
			failed = true;
			break;
		case EXCEPTION:
			failed = true;
			break;
		default:
			failed = false;
			break;
		}
		return failed;
	}

}
